/**
 * Copyright (c) 2022 Beijing Jiaotong University
 * PhotLab is licensed under [Open Source License].
 * You can use this software according to the terms and conditions of the [Open Source License].
 * You may obtain a copy of [Open Source License] at: [https://open.source.license/]
 *
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 *
 * See the [Open Source License] for more details.
 *
 * Author: Zhenjie Wei
 * Created: Sep. 26, 2023
 * Supported by: National Key Research and Development Program of China
 */

import LogicCore from '@/common/logic/core'
import LogicLayer from '@/common/logic/layer'
import { Point, Direction } from '@/common/logic/common/types2D'
import RenderPath from '../models/path'
import RenderPair from '../models/pair'
import { graphManager } from '@/services/phot_client/graph'
import { PortType } from '../models/port'
import { uid, uid2hex, arr2uid } from '@/common/types/base_types'
import { USE_STRAIGHT_MODE } from '../models/path'
import Component from '../objects/comp'

export default class LinkLayer extends LogicLayer {
    private _linking: boolean = false
    private _lastPos: Point = Point.zero()
    private _dirLocked: boolean = false
    private _dirHovered: boolean = false
    private _currDir: Direction = Direction.RIGHT
    private _currPath: RenderPath | null = null

    private _startPair: RenderPair | null = null

    private _linkCache: CanvasRenderingContext2D | null = null
    private _lastHoverredLinkId: uid | null = null
    private _hovering: boolean = false
    private _linkSelected: uid | null = null

    public onMounted(core: LogicCore): void {
        this._linkCache = core.createCache()
        this._linkCache.imageSmoothingEnabled = false
        core.on('mousedown', this._onMouseDown.bind(this), -1)
        core.on('mousemove', this._onMouseMove.bind(this), -1)
        core.on('keydown.shift', () => {
            if (this._dirLocked) return
            this._dirLocked = true
            let dir: string
            switch (this._currDir) {
                case Direction.LEFT:
                    dir = 'LEFT'
                    break
                case Direction.RIGHT:
                    dir = 'RIGHT'
                    break
                case Direction.UP:
                    dir = 'UP'
                    break
                case Direction.DOWN:
                    dir = 'DOWN'
                    break
            }
            this.core?.fire(
                'toast.show',
                `Direction of the link is LOCKED to ${dir}.`
            )
        })
        core.on('keyup.shift', () => {
            this._dirLocked = false
            this.core?.fire('toast.show', 'Direction of the link is UNLOCKED.')
        })
        core.on('pair.click', this._onPairClicked.bind(this))
        core.on('pair.hover', (pair: RenderPair) => {
            if (this._linking && !this._dirLocked) {
                const crd = pair.position()
                this._dirHovered = true
                this._currDir = pair.dir
                this._currPath!.setLastWayPoint(crd, this._currDir)
            }
        })
        core.on('pair.leave', () => {
            if (this._linking) {
                this._dirHovered = false
            }
        })
        core.on('link.hover', (id: uid) => {
            core.setCursor('pointer')
            this._hovering = true
            console.log('hover', id)
            core.render()
        })
        core.on('link.leave', () => {
            core.popCursor('pointer')
            this._hovering = false
            console.log('leave')
            core.render()
        })
        core.on('keydown.delete', () => {
            if (this._linkSelected) {
                this._deleteLink(this._linkSelected)
                this._linkSelected = null
                core.renderAll()
            }
        })
        core.on('keydown.control.alt.l', () => {
            USE_STRAIGHT_MODE.value = !USE_STRAIGHT_MODE.value
            core.renderAll()
        })
    }

    private _deleteLink(id: uid) {
        const renderGraph = graphManager.renderGraph
        const link = renderGraph?.links.get(id)
        if (!link) return
        graphManager.delLink(link.srcPair, link.dstPair)
    }

    private _onPairClicked(pair: RenderPair) {
        if (!this._linking) {
            if (pair.typ === PortType.IN) {
                return // cannot start from an input port
            }
            this._startPair = pair
            this._linking = true
            this._currPath = new RenderPath(pair.position())
            this._currPath.addWayPoint(pair.position(), pair.dir)
            this.core?.fire('link.begin', pair)
            this.core?.fire(
                'toast.show',
                'Hold down SHIFT to lock the direction of the link.'
            )
        } else if (this._startPair?.compatibleWith(pair)) {
            this._currPath?.setLastWayPoint(pair.position(), pair.dir)
            if (!this._currPath) {
                console.error('LinkLayer: _currPath is null')
                return
            }
            graphManager.addLink(this._startPair, pair, this._currPath!)
            this.core?.renderAll()
            this._linking = false
            this._startPair = null
            this.core?.fire('link.end')
            this._updateLinkCache()
        }
    }

    private _onMouseDown(e: MouseEvent) {
        if (e.button === 0) {
            this._linkSelected = null
            if (this._linking) {
                const pos = new Point(e.offsetX, e.offsetY)
                const crd = this.core?.pos2crd(pos)
                if (!crd) return
                const dir = pos.minus(this._lastPos).normalDir
                this._currPath!.addWayPoint(crd, dir)
                return false // stop propagation
            }
            if (this._hovering) {
                const id = this._lastHoverredLinkId!
                this.core?.fire('link.click', id)
                this._linkSelected = id
                return false // stop propagation
            }
        } else if (e.button === 2) {
            if (this._linking) {
                this._linking = false
                this._startPair = null
                this._currPath = null
                return false // stop propagation
            }
        }
    }

    private _queryNearbyLinkId(pos: Point): uid {
        const cacheCtx = this._linkCache!
        const imgData = cacheCtx
            .getImageData(pos.x, pos.y, 1, 1)
            .data.slice(0, 3)
        const id = arr2uid(imgData)
        return id
    }

    private _onMouseMove(e: MouseEvent) {
        const pos = new Point(e.offsetX, e.offsetY)
        if (this._linking) {
            const crd = this.core?.pos2crd(pos)!
            if (!this._dirLocked && !this._dirHovered) {
                const dir = Point.minus(pos, this._lastPos).normalDir
                this._currDir = dir
            }
            this._currPath!.setLastWayPoint(crd, this._currDir)
            // smooth the last position
            this._lastPos.times(0.9).plus(pos.times(0.1))
        } else {
            const id = this._queryNearbyLinkId(pos)
            if (id !== this._lastHoverredLinkId) {
                if (id) {
                    if (!graphManager.renderGraph?.paths.has(id)) {
                        console.warn('path not found', id)
                        return
                    }
                    this._lastHoverredLinkId = id
                    this.core!.fire('link.hover', id)
                } else {
                    this._lastHoverredLinkId = id
                    this.core!.fire('link.leave')
                }
            }
        }
    }

    private _updateLinkCache() {
        const cacheCtx = this._linkCache!
        const core = this.core!
        const paths = graphManager.renderGraph?.paths
        if (!paths) return
        cacheCtx.clearRect(0, 0, core.stageWidth, core.stageHeight)
        cacheCtx.save()
        cacheCtx.lineWidth = 10
        for (const [id, path] of paths) {
            const color = uid2hex(id as uid)
            cacheCtx.strokeStyle = color
            path.strokeOn(cacheCtx, core)
        }
        cacheCtx.restore()
    }

    public onCache(ctx: CanvasRenderingContext2D): boolean {
        const paths = graphManager.renderGraph?.paths
        if (!paths) return false
        ctx.strokeStyle = 'black'
        ctx.lineWidth = 2
        for (const path of paths.values()) {
            path.strokeOn(ctx, this.core!)
        }
        this._updateLinkCache()
        return true
    }

    public onPaint(ctx: CanvasRenderingContext2D): boolean {
        ctx.save()
        if (this._linking) {
            ctx.strokeStyle = 'black'
            ctx.lineWidth = 2
            this._currPath?.strokeOn(ctx, this.core!)
        }
        if (this._hovering) {
            const path = graphManager.renderGraph?.paths.get(
                this._lastHoverredLinkId!
            )
            ctx.strokeStyle = '#aaaaaaaa'
            ctx.lineWidth = 6
            path?.strokeOn(ctx, this.core!)
        }
        if (this._linkSelected) {
            const path = graphManager.renderGraph?.paths.get(
                this._linkSelected!
            )
            ctx.strokeStyle = '#364fc7'
            ctx.lineWidth = 1
            ctx.strokeRect(...this.core!.crd2posRect(path!.area).ltwh)
        }
        ctx.restore()
        return true
    }
}
